'''
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with self work for additional information
 * regarding copyright ownership.  The ASF licenses self file
 * to you under the Apache License, Version 2.0 (the
 * "License") you may not use self file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     http:#www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
'''
from base.graph import Node_ANY
from base.util import IteratorUtil
from base.util.iterator.impl.WrappedIterator import WrappedIterator

#import java.util.ArrayList;
#import java.util.Iterator;
#import java.util.List;
#import java.util.Set;

#import org.apache.jena.atlas.iterator.ite;
#import org.apache.jena.graph.impl.GraphWithPerform;
#import org.apache.jena.util.IteratorCollection;
#import org.apache.jena.util.iterator.ExtendedIterator;
#import org.apache.jena.util.iterator.WrappedIterator;

'''
 * An ad-hoc collection of useful code for graphs
'''



sliceSize = 1000

#private static int 
MIN_SRC_SIZE = 1000
# If source and destination are large, limit the search for the best way round to "deleteFrom"
MAX_SRC_SIZE = 1000 * 1000
DST_SRC_RATIO = 2

CMP_GREATER = 1
CMP_EQUAL = 0
CMP_LESS = -1


class GraphUtil:
    '''
     * Only static methods here - the class cannot be instantiated.
    '''
    # private GraphUtil():

    '''
     * Return an iterator over the unique subjects with predicate p and object o.
     * p and o can be wildcards (Node_ANY.ANY)
     *
     * @param g Graph
     * @param p Predicate - may be Node_ANY.ANY
     * @param o Object  - may be Node_ANY.ANY
     * @return ExtendedIterator
    '''
    # param Graph g, Node p, Node o
    # result static ExtendedIterator<Node>
    @staticmethod
    def listSubjects(g, p, o):
        # Restore a minimal QueryHandler?
        #ExtendedIterator<Triple>
        ite = g.find(Node_ANY.ANY, p, o)
        #Set<Node>
        nodes = ite.mapWith(lambda t:t.getSubject()).toSet()
        return WrappedIterator.createNoRemove(nodes.iterator())
    

    '''
     * Return an iterator over the unique predicate between s and o.
     * s and o can be wildcards (Node_ANY.ANY)
     *
     * @param g Graph
     * @param s Subject - may be Node_ANY.ANY
     * @param o Object  - may be Node_ANY.ANY
     * @return ExtendedIterator
    '''
    # param Graph g, Node s, Node o
    # result static ExtendedIterator<Node>
    @staticmethod
    def listPredicates(g, s, o):
        #ExtendedIterator<Triple>
        ite = g.find(s, Node_ANY.ANY, o)
        #Set<Node>
        nodes = ite.mapWith(lambda t:t.getPredicate()).toSet()
        return WrappedIterator.createNoRemove(nodes.iterator())
    

    '''
     * Return an iterator over the unique objects with a given subject and object.
     * s and p can be wildcards (Node_ANY.ANY)
     *
     * @param g Graph
     * @param s Subject - may be Node_ANY.ANY
     * @param p Predicate  - may be Node_ANY.ANY
     * @return ExtendedIterator
    '''
    # param Graph g, Node s, Node p
    # result static ExtendedIterator<Node>
    @staticmethod
    def listObjects(g, s, p):
        #ExtendedIterator<Triple>
        ite = g.find(s, p, Node_ANY.ANY)
        #Set<Node>
        nodes = ite.mapWith(lambda t:t.getObject()).toSet()
        return WrappedIterator.createNoRemove(nodes.iterator())
    

    '''
     * Does the graph use the node anywhere as a subject, predicate or object?
    '''
    # param Graph graph, Node node
    # result static boolean
    @staticmethod
    def containsNode(graph, node):
        return graph.contains(node, Node_ANY.ANY, Node_ANY.ANY) or graph.contains(Node_ANY.ANY, Node_ANY.ANY, node) or graph.contains(Node_ANY.ANY, node, Node_ANY.ANY)
    

    ''' Control how events are dealt with in bulk'''
    #private static final boolean
    #OldStyle = True

    '''
     * Answer an iterator covering all the triples in the specified graph.
     *
     * @param g the graph from which to extract triples
     * @return an iterator over all the graph's triples
    '''
    # param Graph g
    # result static ExtendedIterator<Triple>
    @staticmethod
    def findAll(g):
        return g.find()
    

    # param Graph graph, Triple[] triples
    # result static void
    @staticmethod
    def add(graph, triples):
        # if (isinstance(graph, GraphWithPerform)):
        #     #GraphWithPerform g = (GraphWithPerform) graph
        #     g = graph
        #     for t in triples:
        #         g.performAdd(t)
        #     #graph.getEventManager().notifyAddArray(graph, triples)
        # else:
        #     for t in triples:
        #         graph.add(t)
        for t in triples:
            graph.add(t)
        
    

    # param Graph graph, List<Triple> triples
    # result static void
    # @classmethod
    # def add(cls, graph, triples):
    #     cls.addIteratorWorkerDirect(graph, triples.iterator())
        # if isinstance(graph, GraphWithPerform):
        #     graph.getEventManager().notifyAddList(graph, triples)
    

    # param Graph graph, Iterator<Triple> it
    # result static void
    # @classmethod
    # def add(cls, graph, it):
    #     if (isinstance(graph, GraphWithPerform)):
    #         # Materialize for the notify.
    #         List<Triple> s = list(it)
    #         cls.addIteratorWorkerDirect(graph, s.iterator())
    #         graph.getEventManager().notifyAddIterator(graph, s)
    #     else:
    #          cls.addIteratorWorker(graph, it)
    

    '''
     * Add triples into the destination (arg 1) from the source (arg 2)
    '''
    # param Graph dstGraph, Graph srcGraph
    # result static void
    @classmethod
    def addInto(cls, dstGraph, srcGraph):
        if dstGraph == srcGraph and not dstGraph.getEventManager().listening():
            return
        dstGraph.getPrefixMapping().setNsPrefixes(srcGraph.getPrefixMapping())
        cls.addIteratorWorker(dstGraph, cls.findAll(srcGraph))
        dstGraph.getEventManager().notifyAddGraph(dstGraph, srcGraph)
    

    # param Graph graph, Iterator<Triple> it
    # result static void
    @classmethod
    def addIteratorWorker(cls, graph, it):
        #List<Triple>
        #s = list(it)
        s = it
        cls.addIteratorWorkerDirect(graph, s.iterator())
    

    # param Graph graph, Iterator<Triple> it
    # result static void
    @staticmethod
    def addIteratorWorkerDirect(graph, it):
        # if (isinstance(graph, GraphWithPerform)):
        #     GraphWithPerform g = (GraphWithPerform) graph
        #     it.forEachRemaining(g::performAdd)
        # else:
        #     it.forEachRemaining(graph::add)
        it.forEachRemaining(graph.add)
        
    

    # param Graph graph
    # result static boolean
    # @staticmethod
    # def requireEvents(graph):
    #     return graph.getEventManager().listening()
    

    # param Graph graph, Triple[] triples
    # result static void
    @staticmethod
    def delete(graph, triples):
        # if (isinstance(graph, GraphWithPerform)):
        #     GraphWithPerform g = (GraphWithPerform) graph
        #     for (Triple t : triples)
        #         g.performDelete(t)
        #     graph.getEventManager().notifyDeleteArray(graph, triples)
        #  else:
        #     for (Triple t : triples)
        #         graph.delete(t)
        for t in triples:
            graph.delete(t)
        
    

    # param Graph graph, List<Triple> triples
    # result static void
    # @staticmethod
    # def delete(graph, triples):
    #     cls.deleteIteratorWorkerDirect(graph, triples.iterator())
    #     if (graph instanceof GraphWithPerform)
    #         graph.getEventManager().notifyDeleteList(graph, triples)
    

    # param Graph graph, Iterator<Triple> it
    # result static void
    # @staticmethod
    # def delete(graph, it):
    #     if (isinstance(graph, GraphWithPerform)):
    #         # Materialize for the notify.
    #         List<Triple> s = list(it)
    #         cls.deleteIteratorWorkerDirect(graph, s.iterator())
    #         graph.getEventManager().notifyDeleteIterator(graph, s)
    #     else:
    #         cls.deleteIteratorWorker(graph, it)
    
    '''
     * A safe and cautious remove() function that converts the remove to
     * a number of:@link Graph#delete(Triple) operations.
     * <p>
     * To avoid any possible ConcurrentModificationExceptions,
     * it finds batches of triples, deletes them and tries again until
     * no more triples matching the input can be found.
    '''
    # param Graph g, Node s, Node p, Node o
    # result static void
    @staticmethod
    def remove(g, s, p, o):
        # Beware of ConcurrentModificationExceptions.
        # Delete in batches.
        # That way, there is no active iterator when a delete
        # from the indexes happens.

        # Triple[] array = new Triple[sliceSize]
        array = []

        while True:
            # Convert/cache s,p,o?
            # The Node Cache will catch these so don't worry unduely.
            # ExtendedIterator<Triple>
            ite = g.find(s, p, o)

            # Get a slice
            #len = 0
            # for ( len < sliceSize len++):
            #     if (!ite.hasNext())
            #         break
            #     array[len] = ite.next()
            le = 0
            for le in range(sliceSize):
                if not ite.hasNext():
                    break
                array[le] = ite.next()

            # Delete them.
            for i in range(le):
                g.delete(array[i])
                array[i] = None
            
            # Finished?
            if le < sliceSize:
                break
        
    

    '''
     * Delete triples in:@code srcGraph from:@code dstGraph
     * by looping on:@code srcGraph.
    '''
    # param Graph dstGraph, Graph srcGraph
    # result static void
    @classmethod
    def deleteLoopSrc(cls, dstGraph, srcGraph):
        cls.deleteIteratorWorker(dstGraph, cls.findAll(srcGraph))
        # dstGraph.getEventManager().notifyDeleteGraph(dstGraph, srcGraph)
    

    '''
     * Delete the triple in:@code srcGraph from:@code dstGraph
     * by checking the contents of:@code dsgGraph against the:@code srcGraph.
     * This involves calling:@code srcGraph.contains.
     *
     * @implNote:@code dstGraph.size() is used by self method.
    '''
    # param Graph dstGraph, Graph srcGraph
    # result static void
    @classmethod
    def deleteLoopDst(cls, dstGraph, srcGraph):
        # Size the list to avoid reallocation on growth.
        # int
        dstSize = dstGraph.size()
        #List<Triple> toBeDeleted = new ArrayList<>(dstSize)
        toBeDeleted = []
        # Iterator<Triple>
        ite = cls.findAll(dstGraph)
        while ite.hasNext():
            #Triple
            t = ite.next()
            if srcGraph.contains(t):
                toBeDeleted.append(t)
        
        cls.deleteIteratorWorkerDirect(dstGraph, IteratorUtil.iterator(toBeDeleted))
        dstGraph.getEventManager().notifyDeleteGraph(dstGraph, srcGraph)
    

    '''
     * Delete the triples supplied by an iterator. This function is "concurrent
     * modification" safe - it internally takes a copy of the iterator.
    '''
    # param Graph graph, Iterator<Triple> it
    # result static void
    @classmethod
    def deleteIteratorWorker(cls, graph, it):
        #List<Triple> s = list(it)
        s = it
        cls.deleteIteratorWorkerDirect(graph, s.iterator())
    

    '''
     * Delete the triples supplied by an iterator. This function is not "concurrent
     * modification" safe it assumes it can use the iterator while deleting from the
     * graph.
    '''
    # param Graph graph, Iterator<Triple> it
    # result static void
    @staticmethod
    def deleteIteratorWorkerDirect(graph, it):
        # if (isinstance(graph, GraphWithPerform)):
        #     GraphWithPerform g = (GraphWithPerform) graph
        #     it.forEachRemaining(g::performDelete)
        # else:
        #     it.forEachRemaining(graph::delete)
        it.forEachRemaining(graph.delete)
        
    

    

    '''
     * Delete triples in the destination (arg 1) as given in the source (arg 2).
     *
     * @implNote This is designed for the case of:@code dstGraph being comparable or much larger than
     *:@code srcGraph or:@code srcGraph having a lot of triples to actually be
     * deleted from:@code dstGraph. This includes the case of large, persistent:@code dstGraph.
     * <p>
     * It is not designed for a large:@code srcGraph and large:@code dstGraph
     * with only a few triples in common to delete from:@code dstGraph. It is better to
     * calculate the difference in some way, and copy into a small graph to use as the:@code srcGraph.
     * <p>
     * To force delete by looping on:@code srcGraph, use:@link #deleteLoopSrc(Graph, Graph).
     * <p>
     * For large:@code srcGraph and small:@code dstGraph, use:@link #deleteLoopDst.
     * <p>
     * See discussion on <a href="https:#github.com/apache/jena/pull/212">jena/pull/212</a>,
     * (archived at <a href="https:#issues.apache.org/jira/browse/JENA-1284">JENA-1284</a>).
    '''
    # param Graph dstGraph, Graph srcGraph
    # result static void
    @classmethod
    def deleteFrom(cls, dstGraph, srcGraph):
        # boolean events = requireEvents(dstGraph)

        if (dstGraph == srcGraph):
            dstGraph.clear()
            return
        
        # boolean
        loopOnSrc = cls.decideHowtoExecute(dstGraph, srcGraph)

        if (loopOnSrc):
            # Normal path.
            cls.deleteLoopSrc(dstGraph, srcGraph)
            return
        

        # Loop on dstGraph, not srcGraph, but need to use srcGraph.contains on self code path.
        cls.deleteLoopDst(dstGraph, srcGraph)
    



    '''
     * Decide whether to loop on dstGraph or srcGraph.
     *
     * @param dstGraph
     * @param srcGraph
     * @return boolean True for "loop on src"
    '''
    # param Graph dstGraph, Graph srcGraph
    # result static boolean
    @classmethod
    def decideHowtoExecute(cls, dstGraph, srcGraph):
        #return decideHowtoExecuteBySizeSize(dstGraph, srcGraph)

        # Avoid calling dstGraph.size()
        return cls.decideHowtoExecuteBySizeStep(dstGraph, srcGraph)
    

    '''
     * Decide using dstGraph.size() and srcGraph.size()
    '''
    # param Graph dstGraph, Graph srcGraph
    # result static boolean
    @staticmethod
    def decideHowtoExecuteBySizeSize(dstGraph, srcGraph):
        # Loop on src if:
        #     size(src) <= MIN_SRC_SIZE : srcGraph is below the threshold MIN_SRC_SIZE (a "Just Do it" number)
        #     size(src)*DST_SRC_RATIO <= size(dst)
        # dstGraph is "much" larger than src where "much" is given by DST_SRC_RATIO
        #     Assumes dstGraph.size is efficient.

        #int
        srcSize = srcGraph.size()
        if (srcSize <= MIN_SRC_SIZE):
            return True
        #int
        dstSize = dstGraph.size()

        #boolean
        loopOnSrc = (srcSize <= MIN_SRC_SIZE or dstSize > DST_SRC_RATIO * srcSize)
        return loopOnSrc
    

    '''
     * Avoid dstGraph.size(). Instead step through:@codedstGraph.find to compare to:@code srcGraph.size()
    '''
    # param Graph dstGraph, Graph srcGraph
    # result static boolean
    @classmethod
    def decideHowtoExecuteBySizeStep(cls, dstGraph, srcGraph):
        # loopOnSrc if:
        #     size(src) <= MIN_SRC_SIZE
        #     size(src)*DST_SRC_RATIO <= |find(dst)|
        #int
        srcSize = srcGraph.size()
        if (srcSize <= MIN_SRC_SIZE):
            return True
        #boolean
        loopOnSrc = (srcSize <= MIN_SRC_SIZE or cls.compareSizeTo(dstGraph, DST_SRC_RATIO * srcSize) == CMP_GREATER)
        return loopOnSrc
    

    '''
     * Compare the size of a graph to:@code size, without calling Graph.size
     * by iterating on:@code graph.find() as necessary.
     * <p>
     * Return -1 , 0, 1 for the comparison.
    '''
    '''package'''
    @classmethod
    def compareSizeTo(cls, graph, size):
        #ExtendedIterator<Triple>
        it = graph.find()
        try:
            #int
            stepsTake = IteratorUtil.step(it, size)
            if (stepsTake < size):
                # Iterator ran out.
                return CMP_LESS
            if (not it.hasNext()):
                # Finsiehd at the same time.
                return CMP_EQUAL
            # Still more to go
            return CMP_GREATER
        finally:
            it.close()
        
    

